/*
 * Author: Xingbo Wang, Yaqin Yang, Ying Yang
 * Database Project
 * Time : spring 2011
 * */

package database;


import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.*;
import java.util.Map.Entry;

import database.impl.Record;

/*
 * Table process class
 * */
public class DataFile {
	private String filename; 
	private Integer recordNum = 0;          //total number of records in datafile
	private int validNum = 0;
	public Vector<Record> records = new Vector<Record>(); // all the records
	// index name mapping column name
	private Map<String, String> indexNameMap = new HashMap<String, String>(); 
	// index(in memory) name mapping index
	private Map<String, Index> inMemIndexNameMap = new HashMap<String, Index>(); 
	// bit vector for validation of records
	private Vector<Boolean> bitmap = new Vector<Boolean>(); 
	private Map<String, Integer> schema = new HashMap<String, Integer>();
	//constructors
	public DataFile() {	}
	public DataFile(String fileName, Integer recordnum) {
		filename = fileName;
		recordNum = recordnum;	
		
	}
	public DataFile(String fileName) {
		filename = fileName;
		
	}

	// create an index on some column with name "indexName"
	public Index createIndex(String indexName, String column) {
		// index name is not unique, throw illegal argument exception
		if (indexNameMap.containsKey(indexName)) {
			throw new IllegalArgumentException();
		} else {
			/*
			// table has already one index on the same column,
			// then let index name point to this index
			if (indexNameMap.containsValue(column)) {
				Index index = index.get(column);
				indexNameMap.put(indexName, column);
				inMemIndexNameMap.put(indexName, column);
				return index;
				// table does not have index on column,
				// then create new index, update indexNameMap and indices
			} else {
			*/	
				if (!schema.containsKey(column)) {
					throw new IllegalArgumentException();
				}
				Index index = new Index(filename, indexName, column, records);
				indexNameMap.put(indexName, column);
				inMemIndexNameMap.put(indexName, index);				
				return index;
			}
		}

	// get record at the position of recordId([0, size()-1])
	public Map<String, String> getRecord(int recordId) {
		int size = records.size() - 1;
		if (recordId > size || recordId < 0) {
			return null;
		} else {
			if (bitmap.elementAt(recordId) == true) {
				return records.elementAt(recordId).getRecord();
			} else {
				return null;
			}
		}
	}

	// insert a record, return record ID; if fail, then return -1
	public int insertRecord(Map<String, String> record) {
		Record rcd = new Record();
		int recordId = -1;
		switch (rcd.setRecord(record, schema)) {
		// record contains columns that not belong to the file
		case 1:
			System.err.println("Record contains columns that not belong to the file.");
			throw new IllegalArgumentException();
		// record has column(s) exceeding maximum allowed length
		case 2:
			System.err.println("Record has column(s) exceeding maximum allowed length.");
			throw new IllegalArgumentException();
		//record is legal
		case 0:
			recordId = records.size();
			rcd.setRecordID(recordId);
			modCount++;
			records.add(rcd);
			++validNum;
			bitmap.add(new Boolean(true));
			++recordNum;
		}
		
		//if there are indices(in memory) over file, then update them
		if (!inMemIndexNameMap.isEmpty()) {
			for (Map.Entry<String, Index> entry : inMemIndexNameMap.entrySet()) {
				entry.getValue().insertRecord(rcd);
			}
		}
		return recordId;
	}
	
	// save file contents to disk
	public void dumpFile() {
		try {			
			// output file records contents to 'filename' file in disk
			FileOutputStream fos = new FileOutputStream(filename);
			ObjectOutputStream oos = new ObjectOutputStream(fos);
			
//			oos.writeObject(this);
			oos.writeInt(records.size());
			oos.writeInt(validNum);
//			oos.writeObject(records);
			for (int i = 0; i < records.size(); i++) {
				oos.writeObject(records.elementAt(i));				
			}
			oos.close();
			fos.close();

			// output attributes of file to 'filename+info' file in disk
			fos = new FileOutputStream(filename + "_info");
			oos = new ObjectOutputStream(fos);
			oos.writeObject(indexNameMap);
			oos.writeObject(schema);
			oos.writeObject(bitmap);
			oos.close();
			fos.close();
		} catch (IOException e) {
			System.err.println("Dump File: IO exception!");
		}		
	}

	//count the valid records
	private Vector<Record> rearrangeRecords() {
		int count = recordNum;
		
		//if 50% of record vector is invalid, then rearrange records vector
		double ratio = count / (records.size()+ 0.5);
		if( ratio < 0.5) {
//			++validNum;
			//rearrange records vector
			int id = 0;
			Vector<Record> newRecords = new Vector<Record>();
			for (int i = 0; i < records.size(); i++) {
				if (bitmap.elementAt(i)) {					
					records.elementAt(i).setRecordID(id);
					newRecords.add(records.elementAt(i));
					++id;
				}
			}
			
			//update all indices
			for(Map.Entry<String, Index> entry : inMemIndexNameMap.entrySet()){
				entry.getValue().updateRecordID(newRecords);
			}	
			
			records.clear();
			records = newRecords;			
			
			bitmap.clear();
			for (int j = 0; j < records.size(); j++) {
				bitmap.add(new Boolean(true));
			}			
						
		
		}		
		return records;
	}


	// print the content of the file in human-readable format
	public String viewFile() {
		String s = "";		
		for (int i = 0; i < records.size(); ++i) {
			if (bitmap.elementAt(i) == true) {
				s += Integer.toString(i) + ":\n";
				s += record_content(records.elementAt(i).getRecord());				
			}
		}
		System.out.println(s);
		return s;
	}

	private String record_content(Map<String, String> record) {
		Iterator it = record.entrySet().iterator();
		String output = "";
		while (it.hasNext()) {
			Map.Entry pairs = (Map.Entry) it.next();
			if (pairs.getValue() != null) {
				output = output + "\t" +(pairs.getKey() + ": " + pairs.getValue())
						+ "\n";
			}

		}
		return output;
	}

	// drop the file
	public void dropFile() {
		// delete indices in memory and on disk
		for (Map.Entry<String, String> entry : indexNameMap.entrySet()) {
			// delete all indices on disk
			String indexname = entry.getKey();
			String name = filename + "_" + indexname;
			delete(name);
		}
		indexNameMap.clear();
		inMemIndexNameMap.clear();		

		// delete all records and info
		String fileInfo =  filename + "_" + "info";
		delete(filename);
		delete(fileInfo);
		records.clear();

		Map<String, DataFile> openFiles = DataManager.getOpenedFiles();
		openFiles.remove(filename);		
	}

	// restore Index with name "indexName"
	public Index restoreIndex(String indexName) {
		FileInputStream fis;
		ObjectInputStream ois;
		Index index = null;
		
		try {
			//index with name "indexName" is already in memory, throw exception
			if (inMemIndexNameMap.containsKey(indexName)) {
				System.err.println("restore index: Index with this name already exist in memory.");
				throw new IllegalArgumentException();
			}
			/*
			//index of this name is not stored on the disk
			if(!indexNameMap.containsKey(indexName)){
				System.err.println("restore index: Index with this name is not stored on disk.");
				throw new IllegalArgumentException();
			}
			*/
			fis = new FileInputStream(filename+"_"+indexName);	
			ois = new ObjectInputStream(fis);
			index=((Index) ois.readObject());
			
			//if Index is out of sync with the file, throw exception
			if(index.getValidNumber() != validNum){
				System.err.println("restore index: Index is out of sync with the file.");
				throw new IllegalStateException();
			}
			inMemIndexNameMap.put(indexName, index);
			
		} catch (FileNotFoundException e) {
			System.err.println("restore index: Index name not exist.");
			throw new IllegalArgumentException();
		} catch (IOException e) {
			System.err.println("restore index: IO exception.");
			e.printStackTrace();
		} catch (ClassNotFoundException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}

		return index;
	}

	// drop index with name "indexName"
	public void dropIndex(String indexName) {
		if (!indexNameMap.containsKey(indexName)) {
			return;
		} else {
			//delete index in memory
			indexNameMap.remove(indexName);
			if (inMemIndexNameMap.containsKey(indexName)){
				inMemIndexNameMap.get(indexName).drop();
				inMemIndexNameMap.remove(indexName);
			}
				
			
			//delete index on disk
			String name = filename + "_" + indexName;
			delete(name);
			
			return;
		}
	}

	public static void delete(String fileName) {
		try {
			// Construct a File object for the file to be deleted.
			File target = new File(fileName);

			if (!target.exists()) {
				System.err.println("File " + fileName
						+ " not present to begin with!");
				return;
			}

			// Quick, now, delete it immediately:
			if (target.delete())
				System.err.println("** Deleted " + fileName + " **");
			else
				System.err.println("Failed to delete " + fileName);
		} catch (SecurityException e) {
			System.err.println("Unable to delete " + fileName + "("
					+ e.getMessage() + ")");
		}
	}
	
	protected Vector<Record> deleteRecord(int recordId){
		Vector<Record> newRecords = null;
		if(bitmap.get(recordId)){ //if record with recordId is valid, then "delete" it
			++validNum;
			bitmap.set(recordId, false);
			--recordNum;
			for(Map.Entry<String, Index> entry : inMemIndexNameMap.entrySet()){
				entry.getValue().deleteRecord(records.get(recordId));
			}	
			newRecords = rearrangeRecords();
		}
		modCount++;
		return newRecords;
	}
	
	// create and return an iterator
	public Iterator<Integer> iterator() {
		return new FileIterator(records);
	}


	protected int modCount;
	// An private Iterator implement class, implement Iterator interface
	private class FileIterator implements Iterator<Integer> {
	// Vector<Integer> tempBitmap = new Vector<Integer>(); 
		Vector<Record> records;
		int numLeft =0;
		//int counterForNext=0;
		int lastPosition = -1;
		int indexForTrue=0;
		int expectedModCount = modCount;
        
		public FileIterator(Vector<Record> records) {
			this.records = records;
			
		/*	for(int i=0;i<bitmap.size();i++){
				if(bitmap.get(i)){
			tempBitmap.add(i); 
			}
			}*/
	
			numLeft =recordNum;
			}
			//for(int i=0;i<tempBitmap.size();i++){
			//System.out.println("tempBitmap"+tempBitmap.get(i));}
			//numLeft=bitmap.size();}
		//numLeft=recordNum;}
		@Override
		public boolean hasNext() {
			return numLeft > 0;
		}

		@Override
		public Integer next() {
			if (expectedModCount != modCount) {
				throw new ConcurrentModificationException();
			}
			try {
				int index=0;
				if (expectedModCount != modCount) {
					throw new ConcurrentModificationException();
				}
			//	int index = tempBitmap.size() - numLeft;
				for(int i=indexForTrue;i<bitmap.size();i++){
					if(bitmap.get(i)){
						index=i;
						break;
					}
				}
				
				//int index=tempBitmap.get(counterForNext);
//				System.out.println("bitmapSize "+bitmap.size()+" numLeft "+numLeft+" This is index in next "+index);
				Integer result = records.get(index).getRecordID();
				lastPosition = index;
				numLeft--;
				indexForTrue=index+1;
				return result;
			} catch (IndexOutOfBoundsException e) {
				System.out.println(e);
				throw new NoSuchElementException();
			}
			
		}

		@Override
		public void remove() {
			if (expectedModCount != modCount) {
				throw new ConcurrentModificationException();
			}
			if (lastPosition == -1) {
				throw new IllegalStateException();
			}

			try {
				if (lastPosition == records.size() - numLeft) {
					numLeft--; // we're removing after a call to previous()
				}
				records = deleteRecord(lastPosition);

				/*bitmap.set(lastPosition, false);
				++validNum;
				rearrangeRecords();
				//validRecordNum--;
				// records.remove(lastPosition);
*/			} catch (IndexOutOfBoundsException e) {
				throw new ConcurrentModificationException();
			}

			lastPosition = -1;
			
			expectedModCount = modCount;

			
		}
		}


	
	//setters and getters
	public void setFileName(String FileName) {
		this.filename = FileName;
	}

	public Vector<Record> getRecords() {
		return this.records;
	}

	public Map<String, String> getIndexNameMap() {
		return indexNameMap;
	}

	public void setIndexNameMap(Map<String, String> indexNameMap) {
		this.indexNameMap = indexNameMap;
	}

	public Map<String, Index> getIndices() {
		return inMemIndexNameMap;
	}	

	public Vector<Boolean> getBitmap() {
		return bitmap;
	}

	public void setBitmap(Vector<Boolean> bitmap) {
		this.bitmap = bitmap;
	}

	public Map<String, Integer> getSchema() {
		return schema;
	}

	public void setSchema(Map<String, Integer> schema) {
		this.schema = schema;
	}

	public int getValidNum() {
		return validNum;
	}

	public void setValidNum(int validNum) {
		this.validNum = validNum;
	}
	public Integer getRecordNum() {
		return recordNum;
	}
	public void setRecordNum(Integer recordNum) {
		this.recordNum = recordNum;
	}

}
